Skip to content

feat(eval): batch-evaluation simulate — each example owns its invoker - #2032

Open
jariy17 wants to merge 2 commits into
refactorfrom
feat/eval-invoke-dataset-pr
Open

feat(eval): batch-evaluation simulate — each example owns its invoker#2032
jariy17 wants to merge 2 commits into
refactorfrom
feat/eval-invoke-dataset-pr

Conversation

@jariy17

@jariy17 jariy17 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

Adds batch-evaluation simulate to replay a dataset against a runtime and grade the resulting sessions.

Each dataset example is a self-running object that owns its own invoker (RunContext). invokeDataset builds one invoker per session and passes it into the example's run(). The example performs the invocation; the machine supplies the invoker and collects the result.

The invoker seam

A dataset example knows what to send and how to turn the responses into ground truth, but not how to reach the runtime. Its RunContext owns that runtime-specific invocation:

// src/core/eval/invokeDataset/example/types.ts
export type TurnResult = { text: string };
export type RunContext = { invokeOnce(input: string): Promise<TurnResult> };

export interface Example {
  readonly exampleId: string;
  readonly schemaType: DatasetSchemaType;
  // Replay this example against the runtime `ctx` reaches, return neutral ground truth.
  run(ctx: RunContext): Promise<InlineGroundTruth | undefined>;
}

invokeDataset resolves the runtime once, then builds a distinct invoker for each client-generated session and passes it to that example:

const results = await runExamples(examples, async (example) => {
  const ctx: RunContext = {
    invokeOnce: async (input) => {
      const res = await invokeRuntime(
        deps,
        { /* resolved runtime, session, and rendered payload */ },
        options,
        signal,
      );
      return { text: /* drained response body */ };
    },
  };

  return example.run(ctx);
});

The example decides how many times to call invokeOnce and in what order. Its invoker decides how each call reaches the runtime for that session.

Layout

src/core/eval/invokeDataset/
├── load.ts                 DatasetLoader — pure JSONL parse → shape-classify → new
├── run.ts                  runExamples — bounded-concurrency pool, failure isolation
├── invokeDataset.test.ts   end-to-end golden coverage of the whole path
└── example/
    ├── types.ts            Example interface, RunContext (the invoker), TurnResult
    ├── predefined.ts       PredefinedExample — replays scripted turns, builds ground truth
    └── simulated.ts        SimulatedExample — not shipped; throws at construction

Plus src/core/invokeRuntime.ts, which extracts runtime invocation from runtime.tsx for reuse by both RuntimeClient and invokeDataset, and src/handlers/eval/batch-evaluation/simulate/index.tsx, which composes invokeDataset with startBatchEvaluation.

Testing

  • bun run typecheck passes.
  • bun test: 1544 pass, 0 fail.
  • invokeDataset.test.ts drives the real EvalClient.invokeDataset over a fake AWS layer with golden fixtures. Its snapshot covers created sessions and inline ground truth across every supported variation while exercising the loader, example classes, concurrency pool, template rendering, and runtime invocation.
  • simulate.test.tsx snapshots the handler's wrapped sessionMetadata.

@codecov-commenter

codecov-commenter commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49805% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.10%. Comparing base (ee854eb) to head (e3e3fc1).
⚠️ Report is 3 commits behind head on refactor.

Files with missing lines Patch % Lines
src/core/eval.tsx 85.26% 14 Missing ⚠️
src/core/eval/invokeDataset/template.ts 91.30% 2 Missing ⚠️
src/core/eval/invokeDataset/example/simulated.ts 92.30% 1 Missing ⚠️
src/core/invokeRuntime.ts 99.35% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #2032      +/-   ##
============================================
- Coverage     97.13%   97.10%   -0.04%     
============================================
  Files           381      388       +7     
  Lines         22786    23158     +372     
============================================
+ Hits          22134    22487     +353     
- Misses          652      671      +19     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 18, 2026
@jariy17
jariy17 force-pushed the feat/eval-invoke-dataset-pr branch 5 times, most recently from 7ac54d4 to 6a2915e Compare August 19, 2026 17:16
@jariy17
jariy17 force-pushed the feat/eval-invoke-dataset-pr branch from 6a2915e to b747257 Compare August 19, 2026 18:45

@Hweinstock Hweinstock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't get to all the examples work yet, but had a few small comments and a question on how we can simplify testing, because it feels pretty gnarly rn.

Comment thread src/io/template.ts Outdated
@@ -0,0 +1,33 @@
import { InputValidationError } from "../errors";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we keep this inside evals until there is an opportunity to re-use it? I feel like the io directory should be reserved for shared abstractions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking the invoke handler could use this but lets leave it in the simulate handler for now.

Comment thread src/io/template.ts Outdated
try {
parsed = JSON.parse(template);
} catch {
throw new InputValidationError(`--${flagName} must be valid JSON`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we wire the cause here?

qualifier?: string;
payloadTemplate: string; // e.g. {"prompt":"{input}"} — {input} is the example's turn input
headers?: [string, string][];
bearerToken?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: is there a reason bearerToken is treated different from other headers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the discriminator that selects the auth path, not an application header. Its presence routes the invoke to CUSTOM_JWT path (src/core/invokeRuntime.ts:73)

flag("dataset-version", "dataset version (with a dataset id)", z.string().optional()),
flag("evaluator", "evaluator id(s) to apply", z.array(z.string()).optional()),
flag("name", "batch evaluation name (unique in the account)", z.string().optional()),
flag("description", "optional description", z.string().optional()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what exactly is this describing? the simulation itself?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be batch-evaluation description. I'll rename this batch-eval-desc, same with name too.

throw new InputValidationError("required option '--name <name>' not specified");

// Ctrl-C aborts the run (invokes, the ingestion wait, the dataset download).
const controller = new AbortController();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh, theres a shared abstraction for this, but looks like it hasn't been merged yet :(

#1986

maybe we can swap it over as a follow-up once its merged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing

Comment thread src/core/eval.tsx Outdated
const groundTruth = await example.run(ctx);
return { exampleId: example.exampleId, sessionId, groundTruth };
} catch (error) {
this.logger.debug(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of logging and rethrowing, is there a way to enrich the error thrown to avoid noise?

Comment thread src/core/eval.tsx

// AgentCore emits spans ~30s-3min after invoke; grade too early and it reads an empty
// log group and fails every session. Disabled via SIMULATE_INGESTION_WAIT_MS=0 (tests).
const waitMs = Number(process.env.SIMULATE_INGESTION_WAIT_MS ?? 180_000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there anything we can poll on instead of a static wait time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't poll right away because some traces might still be arriving, and I didn't want us to end up with incomplete session data.


// A fake AWS layer: control resolves the runtime, data answers each invoke. Records every
// payload it was asked to send, and per `opts` can fail or delay specific invokes.
function fakeClients(opts: { fail?: (payload: string) => boolean; delayMs?: number } = {}): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like a really complex testing setup. Is there a simpler way? I'm wondering if there's a structural change we could make to simplify here.

Comment thread src/core/eval/invokeDataset/load.ts Outdated
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we wire the cause here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

import type { DatasetSchemaType } from "@aws-sdk/client-bedrock-agentcore-control";
import type { InlineGroundTruth } from "@aws-sdk/client-bedrock-agentcore";

// A record, not a bare string, so a future tool-branching type can widen it by a field.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like the code explains this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing

- move renderJsonTemplate out of shared src/io into core/eval/invokeDataset
- invokeRuntime: raw TypeError/Error -> InputValidationError/RuntimeInvokeResponseError
- wire error causes in template + dataset JSON parse
- invokeDataset: enrich per-example invoke failure instead of log+rethrow
- simulate: bubble Ctrl-C cancellation (telemetry) instead of quiet return; clarify --description help; TODO(#1986) shared abort helper
- drop type-guaranteed 'no leak' test; keep AbortSignal wiring in composition test
- trim stale/redundant comments (runtime.tsx, invokeRuntime DTO note, TurnResult)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants